package edu.deltacollege.portal.portlets.emailpreview;

import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

import javax.mail.Address;
import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.search.FlagTerm;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Delegates filtering of read/unread messages to the server. The
 * implementation is still not ideal from a performance perspective.
 * But server-side sort operations are not commonly supported so
 * we are still downloading headers for all unread messages for
 * a local sort.
 * 
 * <p>Be sure to see Javadoc on {@link #setSortableSequenceNumbers(boolean)}.
 * The default configuration trades performance for potentially
 * unexpected behaviors when messages in the queried folder have
 * been moved there from other folders. The default configuration
 * also might not work with all IMAP providers -- check to see if 
 * message ordering is what you expect.</p>
 */
public class SearchingEmailMessageDAO implements EmailMessageDAO {

    private MessageComparator comparator;
    private int maxUnreadMessages;
    private boolean sortableSequenceNumbers = true;

    private Log log;

    public SearchingEmailMessageDAO () {
        this.log = LogFactory.getLog(SearchingEmailMessageDAO.class);
        comparator = new MessageComparator(MessageComparator.DESC_ORDER);
    }

    public EmailMessageLookupResult getUnreadEmailMessages(Folder mailFolder) 
        throws MessagingException {
        
        long startTime = System.currentTimeMillis();
        
        int inspectedMessageCount = this.getTotalEmailMessageCount(mailFolder);
        if (log.isDebugEnabled()) {
            log.debug("Time elapsed while getting inbox total message count: " +
                (System.currentTimeMillis() - startTime) + 
                " (total message count: " + inspectedMessageCount+ ")");
        }

        startTime = System.currentTimeMillis();
        Message[] allUnreadMessages = mailFolder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
        if (log.isDebugEnabled()) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            log.debug("Time elapsed while getting unseen message IDs:" + elapsedTime + 
                " (message count: " + allUnreadMessages.length + 
                ", time per message: " + (allUnreadMessages.length == 0 ? 0 : (elapsedTime / allUnreadMessages.length)) + ")");
        }
        
        Message[] fetchedUnreadMessages = null;
        if ( sortableSequenceNumbers ) {
            Arrays.sort(allUnreadMessages, new Comparator<Message>() {
                public int compare(Message msg1, Message msg2) {
                    return msg1.getMessageNumber() < msg2.getMessageNumber() ? 1 : -1;
                }
            });
            
            FetchProfile profile = new FetchProfile();
            profile.add(FetchProfile.Item.ENVELOPE);
            
            fetchedUnreadMessages = new Message[Math.min(allUnreadMessages.length, maxUnreadMessages)];
            System.arraycopy(allUnreadMessages, 0, fetchedUnreadMessages, 0, Math.min(allUnreadMessages.length, maxUnreadMessages));
            
            startTime = System.currentTimeMillis();
            mailFolder.fetch(fetchedUnreadMessages, profile);
            if (log.isDebugEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                log.debug("Time elapsed fetching all unseen messages:" + elapsedTime + 
                    " (message count: " + fetchedUnreadMessages.length + 
                    ", time per message: " + (fetchedUnreadMessages.length == 0 ? 0 : (elapsedTime / fetchedUnreadMessages.length)) + ")");
            }
        } else {
            fetchedUnreadMessages = allUnreadMessages;
            FetchProfile profile = new FetchProfile();
            profile.add(FetchProfile.Item.ENVELOPE);
        
            startTime = System.currentTimeMillis();
            mailFolder.fetch(fetchedUnreadMessages, profile);
            if (log.isDebugEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                log.debug("Time elapsed fetching all unseen messages:" + elapsedTime + 
                        " (message count: " + fetchedUnreadMessages.length + 
                        ", time per message: " + (fetchedUnreadMessages.length == 0 ? 0 : (elapsedTime / fetchedUnreadMessages.length)) + ")");
            }
        
            // Sort messages by sent date
            startTime = System.currentTimeMillis();
            Arrays.sort(fetchedUnreadMessages, comparator);
            if (log.isDebugEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                log.debug("Time elapsed sorting all unseen messages:" + elapsedTime + 
                        " (message count: " + fetchedUnreadMessages.length + 
                        ", time per message: " + (fetchedUnreadMessages.length == 0 ? 0 : (elapsedTime / fetchedUnreadMessages.length)) + ")");
            }
        }
        
        List<EmailMessage> unreadEmails = new LinkedList<EmailMessage>();
        for ( Message message : fetchedUnreadMessages ) {
            EmailMessage emailMessage = new EmailMessage();

            // Set sender address
            Address[] addresses = message.getFrom();
            String sender = addresses[0].toString();
            emailMessage.setSender(sender);

            // Set subject and sent date
            emailMessage.setSubject(message.getSubject());
            emailMessage.setSentDate(message.getSentDate());

            unreadEmails.add(emailMessage);
            if ( unreadEmails.size() >= maxUnreadMessages ) {
                break;
            }
        }
        
        return new EmailMessageLookupResult(unreadEmails, inspectedMessageCount);
    }
    
    public int getTotalEmailMessageCount(Folder inbox) throws MessagingException {
        return inbox.getMessageCount();
    }
    
    /**
     * Requires IMAP. Does not actually check to see if the mail
     * provider itself support the SEARCH command.
     */
    public boolean supports(AccountInfoDAO accountInfoDAO) {
        return accountInfoDAO.getMailStoreProtocol() != null && 
            accountInfoDAO.getMailStoreProtocol().startsWith(AccountInfoDAO.IMAP_PROTOCOL_PREFIX);
    }
    
    /**
     * The maximum number of unread messages to retrieve information for.
     *
     * @return The maximum number of unread messages to retrieve.
     */
    public int getMaxUnreadMessages() {
        return this.maxUnreadMessages;
    }

    /**
     * Sets the maximum number of unread messages to retrieve information for.
     *
     * @param maxUnreadMessages The maximum number of unread messages to retrieve.
     */
    public void setMaxUnreadMessages(int maxUnreadMessages) {
        this.maxUnreadMessages = maxUnreadMessages;
    }
    
    /**
     * Is this DAO assumes sequence numbers reflect the order in
     * which messages were originally added to a folder.
     * 
     * @see #isSortableSequenceNumbers()
     * @return
     */
    public boolean isSortableSequenceNumbers() {
        return sortableSequenceNumbers;
    }

    /**
     * If true, will assume that sequence numbering reflects the order in
     * which messages were added to a folder. I.e. older messages have
     * lower sequence numbers than do newer messages. This allows for
     * drastic performance optimization because message headers do not
     * need to be downloaded into order to sort messages by date. This
     * may not be a valid assumption for all IMAP providers, though.
     * Defaults to <code>true</code>. <em>And the end result probably
     * isn't what one would probably expect when moving messages between 
     * folders b/c, for example, Gmail will give a message restored from
     * the trash the next sequence number rather than its original
     * sequence number.</em>
     * 
     * @param sortableSequenceNumbers
     */
    public void setSortableSequenceNumbers(boolean sortableSequenceNumbers) {
        this.sortableSequenceNumbers = sortableSequenceNumbers;
    }

}
